<?php

namespace Kangcg\Qiniu;

use Kangcg\Application\Component;
use Kangcg\Application\Helper\Curl;
use Kangcg\Tencent\Library\V3\V2;

/**
 * 七牛云
 * Class Qiniu
 * @property string access     // 应用的AccessKey
 * @property string secret  //应用的 SecretKey
 * @property string region  //区域
 * @property string url
 * @package Kang\Libs\Tencent
 */
class Qiniu extends Component
{
    const REGION_DEFAULT = 'z2';
    const REGION_Z0 = 'z0'; //华东-浙江
    const REGION_CH_EAST_2 = 'cn-east-2'; //华东-浙江2
    const REGION_Z1 = 'z1'; //华北-河北
    const REGION_Z2 = 'z2'; //华南-广东
    const REGION_CN = 'cn-northwest-1'; //西北-陕西1
    const REGION_NA0 = 'na0'; //北美-洛杉矶
    const REGION_AS0 = 'as0'; //亚太-新加坡
    const REGION_AP_2 = 'ap-southeast-2'; //亚太-河内
    const REGION_AP_3 = 'ap-southeast-3'; //亚太-胡志明

    const SPACE = 'uc.qiniuapi.com';

    const UPLOAD = 'up-{region}.qiniup.com'; //源站上传
    const DOWNLOAD = 'iovip-{region}.qiniup.com'; //源站下载

    const OBJECT_MANAG = 'rs-{region}.qiniuapi.com'; //对象管理
    const OBJECT_ENUM = 'rsf-{region}.qiniuapi.com'; //对象列举
    const QUERY = 'api.qiniuapi.com'; //计量查询

    public function bucketList()
    {
        $url = $this->getHost(self::SPACE) . '/buckets';
        return $this->httpsGet($url);
    }

    public function bucketCreate(string $name)
    {
        $url = $this->getHost() . '/mkbucketv3/' . $name;
        return $this->httpsPost($url);
    }

    /**
     * @param string $filePath 文件路径
     * @param string $bucket 七牛存储空间
     * @param string $fileName 文件名称
     * @return bool|mixed
     */
    public function uploadFile(string $filePath, string $bucket, string $fileName = '')
    {
        $url = $this->getHost(self::UPLOAD);
        if (!file_exists($filePath)) {
            return $this->setError('文件不存在！');
        }

        if (!$file = fopen($filePath, 'rb')) {
            return $this->setError('文件打不开！');
        }

        $fileName = $fileName ? $fileName : basename($filePath);
        $stat = fstat($file);
        $size = $stat['size'];
        $content = fread($file, $size);
        $fields['token'] = $this->getUploadToken($bucket, $fileName);
        $fields['crc32'] = $this->crc32ToData($content);
        $fields['key'] = $fileName;
        $mimeBoundary = md5(microtime());
        $data = [];
        foreach ($fields as $key => $val) {
            array_push($data, '--' . $mimeBoundary);
            array_push($data, "Content-Disposition: form-data; name=\"$key\"");
            array_push($data, '');
            array_push($data, $val);
        }

        array_push($data, '--' . $mimeBoundary);
        $finalMimeType = empty($mimeType) ? 'application/octet-stream' : $mimeType;
        $finalFileName = self::escapeQuotes($fileName);
        array_push($data, "Content-Disposition: form-data; name=\"file\"; filename=\"$finalFileName\"");
        array_push($data, "Content-Type: $finalMimeType");
        array_push($data, '');
        array_push($data, $content);
        array_push($data, '--' . $mimeBoundary . '--');
        array_push($data, '');

        $body = implode("\r\n", $data);
        $contentType = 'multipart/form-data; boundary=' . $mimeBoundary;
        $headers['Content-Type'] = $contentType;

        return $this->uploadPost($url, $headers, $body);
    }

    private function uploadPost($url, $headers, $body)
    {
        $ch = curl_init();
        $option = [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HEADER => true,
            CURLOPT_NOBODY => false,
            CURLOPT_CUSTOMREQUEST => 'POST',
            CURLOPT_URL => $url,
        ];

        if (!ini_get('safe_mode') && !ini_get('open_basedir')) {
            $option[CURLOPT_FOLLOWLOCATION] = true;
        }

        if (!empty($headers)) {
            $header = [];
            foreach ($headers as $key => $val) {
                array_push($header, "$key: $val");
            }

            $option[CURLOPT_HTTPHEADER] = $header;
        }

        curl_setopt($ch, CURLOPT_HTTPHEADER, ['Expect:']);
        $option[CURLOPT_POSTFIELDS] = $body;
        curl_setopt_array($ch, $option);
        $result = curl_exec($ch);
        $ret = curl_errno($ch);
        if ($ret !== 0) {
            $this->setError(curl_error($ch));
            curl_close($ch);
            return false;
        }

        $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
        $body = substr($result, $header_size);
        return json_decode($body, true);
    }

    private static function escapeQuotes($str)
    {
        if (is_null($str)) {
            return null;
        }

        $find = array("\\", "\"");
        $replace = array("\\\\", "\\\"");
        return str_replace($find, $replace, $str);
    }

    private function crc32ToData($data)
    {
        $hash = hash('crc32b', $data);
        $array = unpack('N', pack('H*', $hash));
        return sprintf('%u', $array[1]);
    }

    public function getHost(string $host)
    {
        return str_replace('{region}', $this->getRegion(), $host);
    }

    public function getRegion()
    {
        return $this->_config['region'] ?? self::REGION_DEFAULT;
    }

    public function httpsPost($url, array $body = [], array $headers = ['Content-Type' => 'application/x-www-form-urlencoded'])
    {
        return $this->httpRequest($url, 'POST', $headers, $body);
    }

    public function httpsGet($url, array $headers = ['Content-Type' => 'application/x-www-form-urlencoded'])
    {
        return $this->httpRequest($url, 'GET', $headers);
    }


    public function httpRequest(string $url, string $method, array $headers = [], array $body = [])
    {
        $url = 'https://' . $url;
        $headers = $this->authorization($url, $method, $headers);
        $curl = Curl::getInstall();
        $curl->setHeader($headers);

        return $curl->request($url, $body, $method);
    }

    public function getUploadToken($bucket, $key = null, $expires = 3600, $policy = null, $strictPolicy = true)
    {
        $deadline = time() + $expires;
        $scope = $bucket;
        if ($key !== null) {
            $scope .= ':' . $key;
        }

        $args = self::copyPolicy($args, $policy, $strictPolicy);
        $args['scope'] = $scope;
        $args['deadline'] = $deadline;

        $b = json_encode($args);
        return $this->signWithData($b);
    }

    private function authorization($url, $method, array $headers = [], $body = null)
    {
        $headers['X-Qiniu-Date'] = gmdate('Ymd\THis\Z', time());
        $headers['Authorization'] = 'Qiniu ' . $this->signQiniuAuthorization($url, $method, $body, $headers);

        return $headers;
    }


    private function signQiniuAuthorization(string $url, string $method = "GET", $body = "", array $headers = null)
    {
        $info = parse_url($url);
        if ($method === "") {
            $string = "GET ";
        } else {
            $string = $method . " ";
        }

        if (isset($info["path"])) {
            $string .= $info["path"];
        }
        if (isset($info["query"])) {
            $string .= "?" . $info["query"];
        }

        $string .= "\n";
        $string .= "Host: ";
        if (isset($info["host"])) {
            $string .= $info["host"];
        }

        if (isset($info["port"]) && $info["port"] > 0) {
            $string .= ":" . $info["port"];
        }

        if ($headers != null && isset($headers["Content-Type"])) {
            $string .= "\n";
            $string .= "Content-Type: " . $headers["Content-Type"];
        }

        if ($headers != null) {
            $headerLines = [];
            $keyPrefix = "X-Qiniu-";
            foreach ($headers as $k => $v) {
                if (strlen($k) > strlen($keyPrefix) && strpos($k, $keyPrefix) === 0) {
                    array_push($headerLines, $k . ": " . $v);
                }
            }

            if (count($headerLines) > 0) {
                $string .= "\n";
                sort($headerLines);
                $string .= implode("\n", $headerLines);
            }
        }

        $string .= "\n\n";
        if (!is_null($body)
            && strlen($body) > 0
            && isset($headers["Content-Type"])
            && $headers["Content-Type"] != "application/octet-stream"
        ) {
            $string .= $body;
        }

        return $this->sign($string);
    }

    private function sign($data)
    {
        $hmac = hash_hmac('sha1', $data, $this->secret, true);
        return $this->access . ':' . $this->base64UrlSafeEncode($hmac);
    }

    private function base64UrlSafeEncode($hmac)
    {
        $find = ['+', '/'];
        $replace = ['-', '_'];
        return str_replace($find, $replace, base64_encode($hmac));
    }

    private static function copyPolicy(&$policy, $originPolicy, $strictPolicy)
    {
        if ($originPolicy === null) {
            return [];
        }

        foreach ($originPolicy as $key => $value) {
            if (!$strictPolicy || in_array((string)$key, self::$policyFields, true)) {
                $policy[$key] = $value;
            }
        }

        return $policy;
    }

    public function signWithData($data)
    {
        $encodedData = $this->base64UrlSafeEncode($data);
        return $this->sign($encodedData) . ':' . $encodedData;
    }

    private static function parseRawText($raw)
    {
        $multipleHeaders = explode("\r\n\r\n", trim($raw));
        $headers = array();
        $headerLines = explode("\r\n", end($multipleHeaders));
        foreach ($headerLines as $line) {
            $headerLine = trim($line);
            $kv = explode(':', $headerLine);
            if (count($kv) <= 1) {
                continue;
            }
            // for http2 [Pseudo-Header Fields](https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.1)
            if ($kv[0] == "") {
                $fieldName = ":" . $kv[1];
            } else {
                $fieldName = $kv[0];
            }
            $fieldValue = trim(substr($headerLine, strlen($fieldName . ":")));
            if (isset($headers[$fieldName])) {
                array_push($headers[$fieldName], $fieldValue);
            } else {
                $headers[$fieldName] = array($fieldValue);
            }
        }
        return $headers;
    }

    private static $policyFields = [
        'callbackUrl',
        'callbackBody',
        'callbackHost',
        'callbackBodyType',
        'callbackFetchKey',

        'returnUrl',
        'returnBody',

        'endUser',
        'saveKey',
        'forceSaveKey',
        'insertOnly',

        'detectMime',
        'mimeLimit',
        'fsizeMin',
        'fsizeLimit',

        'persistentOps', // 与 persistentWorkflowTemplateID 二选一
        'persistentNotifyUrl',
        'persistentPipeline',
        'persistentType', // 为 `1` 时开启闲时任务
        'persistentWorkflowTemplateID', // 与 persistentOps 二选一

        'deleteAfterDays',
        'fileType',
        'isPrefixalScope',

        'transform', // deprecated
        'transformFallbackKey', // deprecated
        'transformFallbackMode', // deprecated
    ];
}
